Skip to content

Add Anthropic (Claude) API backend#30

Merged
bddap merged 25 commits into
bddap:mainfrom
bddap-bot:anthropic-backend
Jun 2, 2026
Merged

Add Anthropic (Claude) API backend#30
bddap merged 25 commits into
bddap:mainfrom
bddap-bot:anthropic-backend

Conversation

@bddap-bot

@bddap-bot bddap-bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Hooks refac into Anthropic via API key, as requested.

What changed

  • Default backend is Claude (Messages API, claude-opus-4-8). OpenAI still supported; default model gpt-5.5.
  • Provider selection: explicit provider (config or REFAC_PROVIDER) wins; unset → inferred from which API keys are configured (only OpenAI → OpenAI; Anthropic-only/both/neither → Anthropic). refac login [--provider anthropic|openai], else an interactive pick.
  • Provider-agnostic core Message: { role: Role, fields: Vec<String>, cache: bool }. A turn carries one or more text fields (a transform user turn is [selected, transform]); cache marks the last turn of a static prefix. Each backend adapts it:
    • Anthropic (src/anthropic.rs): /v1/messages via reqwest; each field → a content block; empty fields render as (empty) (the API rejects empty text); cachecache_control: ephemeral on the turn's last block (caches system + few-shot, so repeat calls only pay for the appended input); System → top-level system.
    • OpenAI (src/main.rs): its own OpenAiMessage wire type; the adapter joins a turn's fields and drops cache.
  • Config: provider (optional), model (optional, defaulted per provider). Secrets hold either/both keys; honor ANTHROPIC_API_KEY / OPENAI_API_KEY.

Testing

  • cargo test — 5 passing (request-builder shape + cache placement, empty-field (empty) rendering, no-system, provider inference + override).
  • Live-tested against Anthropic: a normal transform (me like toast / correct grammarI like toast.) and an empty-selected generate ("" / command to list files recursivelyfind .).

🤖 Generated with Claude Code

bddap-bot and others added 2 commits May 29, 2026 10:44
refac now defaults to the Claude Messages API (model claude-opus-4-8); OpenAI
remains available via `provider = "openai"`. New `src/anthropic.rs` talks to the
REST API with reqwest (no official Rust SDK): x-api-key + anthropic-version
headers, top-level `system`, and required `max_tokens`. It adapts refac's flat
message list to Anthropic's shape — lifts the system prompt out of messages and
merges the consecutive user turns to satisfy user/assistant alternation — and
marks the static system prompt + few-shot examples `cache_control: ephemeral`
so repeat calls only pay for the varying input. Config gains `provider`, optional
`model` (defaulted per provider), and `max_tokens`; secrets hold either/both keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Some few-shot samples have an empty `selected`; Anthropic rejects empty text
content blocks (OpenAI tolerated them), so skip empty messages when building the
request. Add a REFAC_DEBUG env that dumps the request JSON. Verified end-to-end:
'Me like toast.' / 'Correct grammar.' -> 'I like toast.'
Comment thread src/anthropic.rs Outdated
Comment thread src/anthropic.rs Outdated
…intln

Per review: drop the ad-hoc REFAC_DEBUG env-gated eprintln and use the existing
tracing setup, gated by the subscriber's level filter like the rest of the code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/anthropic.rs Outdated
Comment thread src/anthropic.rs Outdated
Comment thread src/config_files.rs Outdated
Comment thread src/config_files.rs Outdated
Per review: `provider` is now optional. When it isn't set explicitly (config
file or REFAC_PROVIDER), resolve it from which keys are configured — only an
OpenAI key -> OpenAI; Anthropic-only, both, or neither -> lean Anthropic. An
explicit choice still wins. Adds resolve_provider() + tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/config_files.rs Outdated
Comment thread src/config_files.rs Outdated
Per review: a `max_tokens` config knob is a representable invalid state —
`provider = "openai"` + `max_tokens` does nothing (the OpenAI path ignores it).
Remove it. The Messages API still requires `max_tokens`, so hardcode it as a
constant in the anthropic module. Can be reintroduced later with real support
in both backends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/config_files.rs Outdated
Per review:
- REFAC_PROVIDER no longer silently falls back to Anthropic on an unrecognized
  value — it errors with the accepted options.
- OpenAI default model o1 -> gpt-5.5 (current flagship).
- Drop a WHAT doc-comment on model().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/main.rs Outdated
Per review: `refac login` now accepts `--provider anthropic|openai`; without it,
the user selects a provider via a dialoguer prompt rather than the key-inference
heuristic (you're choosing which key to add). Provider derives clap::ValueEnum.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bddap

bddap commented Jun 2, 2026

Copy link
Copy Markdown
Owner

we have a merge conflict

bddap-bot and others added 2 commits June 2, 2026 11:19
Per review: the prompt-cache breakpoint was inferred from message structure
(last assistant turn), which baked in refac's usage pattern. Take the static
prefix length from the caller instead — refactor() passes chat_prefix().len(),
the part that's fixed across calls. build_request places the breakpoint at that
boundary and never groups a varying turn into the cached prefix. Also corrected
stale comments (alternation is no longer required; merging is just grouping).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bddap-bot

Copy link
Copy Markdown
Contributor Author

Resolved in 7268ea8 — merged main in. The only conflict was the rpassword bump (#29); took 7.5.0 and kept the new dialoguer dep. Builds + tests green; PR shows mergeable again.

Comment thread src/anthropic.rs Outdated
Comment thread src/anthropic.rs Outdated
Per review: that claim could go stale; omit it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/anthropic.rs Outdated
Comment thread src/anthropic.rs Outdated
Comment thread src/anthropic.rs Outdated
Per review:
- module doc was tmi / drifting — cut to a one-liner.
- removed the verbose MAX_TOKENS comment (kept the const; the API requires the field).
- replaced the stringly-typed content/cache `type` tags with enums (BlockType, CacheType).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/anthropic.rs Outdated
Comment thread src/config_files.rs Outdated
The match arms say it; the doc comment carries the why.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/config_files.rs Outdated
Per review. Also drop the WHAT comments restating the test assertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/main.rs
Comment thread src/main.rs Outdated
Per review: drop the mirrored label array; map choices through Debug instead.
Also drop a comment per suggestion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/anthropic.rs Outdated
bddap-bot and others added 2 commits June 2, 2026 12:11
Message is now `{ role: Role, fields: Vec<String>, cache: bool }` instead of a
single-string OpenAI-shaped struct. A turn carries one or more text fields (a
transform user turn is [selected, transform]); `cache` marks the last turn of a
static prefix.

- anthropic: each field -> a content block; empty fields render as `(empty)`
  (the API rejects empty text); `cache` -> cache_control on the turn's last
  block. Drops the old consecutive-same-role merge and the cache_prefix_len
  parameter — the data model carries both now.
- openai: its own `OpenAiMessage` wire type; the adapter joins a turn's fields
  and drops `cache`.
- prompt: chat_prefix marks its last message cached.

Live-tested against Anthropic: a normal transform and an empty-selected
generate ("find .") both work. 5 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review: joining a turn's fields into one string blurs the selected text
into the transform with no reliable separator. Send each field as a separate
message instead (same as the pre-refactor OpenAI path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/anthropic.rs Outdated
~$2.00 of Opus 4.8 output at $25/M. Verified the API accepts it for the model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/anthropic.rs Outdated
Comment thread src/anthropic.rs Outdated
Comment thread src/api.rs Outdated
Per review:
- CacheControl and the content block are internally-tagged unions; use
  `#[serde(tag = "type")]` enums instead of a struct with a manual `type` field.
- message roles are `Role` enums (anthropic ChatMessage and OpenAiMessage), not
  strings; Role gains a lowercase serde repr and `as_str` goes away.
- the response block is a tagged enum (`Text { text } | Other`) rather than a
  stringly `kind` compared against "text".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/api.rs
api.rs is now just the provider-agnostic core (`Message`, `Role`). OpenAI's wire
types and `complete` move to a new openai.rs; the Anthropic-only
`field_or_placeholder` moves into anthropic.rs (and OpenAI no longer applies it
— it tolerates empty content). Also drops the unused OpenAI edits-API types
(EditRequest/EditResponse/Choice), which were dead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/prompt.rs Outdated
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread README.md Outdated
Comment thread README.md Outdated
Drop the removed `max_tokens` config setting, note provider is inferred from
keys when unset, fix the API-key link to Anthropic (the default), and reword
"still supported" -> "also supported".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread README.md Outdated
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bddap

bddap commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Almost ready. Do a sub-agent reviewer loop, then regen the examples in readme using latest opus.

bddap-bot and others added 2 commits June 2, 2026 14:05
Review-loop findings:
- the saved secrets file held the API key but was created world-readable
  (default 0644); create/force it 0600 on unix.
- LogEntry.provider was a Debug-formatted string; use the Provider enum so the
  log matches the config-file casing and stays typed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bddap-bot

Copy link
Copy Markdown
Contributor Author

Done — review loop + example regen.

Sub-agent review loop (3 agents: correctness, security, design/taste) over the full diff. Fixes landed in eca0c21:

  • Security (the one real finding): secrets.toml was written with default perms (0644) — your API key was world-readable on a multi-user box. Now created/forced 0600 on unix.
  • LogEntry.provider was a format!("{:?}") string; now the Provider enum, so the log matches the config casing (anthropic, not Anthropic).

No correctness or injection bugs found. User text is serialized via serde (never concatenated into JSON), the key never hits logs or error bodies.

Judgment calls I left (flagging, not blocking — your call):

  • login --provider openai doesn't persist the choice; it only stores the key, and inference leans Anthropic when both keys exist — so picking OpenAI at login can be silently overridden. Could have login write provider to config. Left it as your current design + REFAC_PROVIDER escape hatch.
  • Message.cache is a public bool set by post-construction mutation; a .cached() builder would be tidier. Minor.
  • fields: Vec<String> (anonymous positional) — reviewers agreed it's acceptable given the adapters iterate fields uniformly.

README examples regenerated with Opus 4.8 (610e2db) — all 7 are fresh real outputs.

@bddap bddap merged commit a5aefe2 into bddap:main Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants